5.3 Ereignisse eines Objekts
 
Die meisten Objekte, mit denen wir es täglich zu tun haben, reagieren auf Anstöße von außen: Ein Auto kann hupen und fahren, eine Person gehen und sprechen. Äußere Anstöße können – projiziert auf den Programmentwurf – Methodenaufrufen gleichgesetzt werden. Ein Client verwaltet beispielsweise ein Objekt vom Typ Car und ruft die Methode Fahren auf. Daraufhin setzt sich das Auto in Bewegung – zumindest aus dem programmtechnischen Blickwinkel heraus.
Die Erfahrung des täglichen Lebens zeigt aber auch, dass Objekte auf diese Anstöße ihrerseits selbst reagieren können. Stellen Sie sich vor, Sie würden in einem Mietshaus wohnen und die Lautstärke der Stereoanlage zu hoch drehen. Wenn Sie Glück haben, werden Ihre Nachbarn das stillschweigend akzeptieren, möglicherweise werden Sie aber das Klingeln an der Wohnungstür vernehmen und einem vielleicht freundlich, vielleicht auch verärgert um mehr Ruhe bittenden Nachbarn entgegentreten.
Ein Methodenaufruf ist der Anstoß eines Clients an ein Objekt, damit dieses eine bestimmte Verhaltensweise zeigt. Die Konsequenz eines Methodenaufrufs könnte sein, dass das Objekt seinerseits beim Aufrufer eine Reaktion auslöst. Diese Reaktion lässt sich ebenfalls programmiertechnisch erfassen: Sie wird als Ereignis oder auch mit dem englischen Begriff Event bezeichnet. Ereignisse spielen eine herausragende Rolle bei der Programmierung grafischer Benutzeroberflächen. Ereignisse lassen sich so abstrahieren, dass sie als Nachrichtenverkehr zwischen einer Ereignisquelle und einem Ereignisempfänger angesehen werden können. Eine Ereignisquelle könnte beispielsweise die Schaltfläche in einem Windows-Fenster sein. Sobald der Anwender mit der Maus auf die Schaltfläche klickt, wird ein Ereignis ausgelöst, auf das der Ereignisempfänger reagieren kann, aber nicht muss.
Die Richtung eines Methodenaufrufs geht immer vom Aufrufer zum Objekt. Das Objekt führt danach die Methode aus. Die Richtung eines Ereignisses ist genau entgegengesetzt: Sie geht vom Objekt zurück zum Aufrufer der Objektmethode, dem Ereignisempfänger. Unter diesem Blickwinkel betrachtet ruft ein Objekt als Ereignisquelle eine ihm bekannte Methode im Client, dem Ereignisempfänger, auf.
Der Zeitpunkt der Ereignisauslösung ist im Code eines Objekts festgelegt. Das Besondere an einem Event ist, dass der Ereignisempfänger auf die Auslösung ganz individuell reagieren kann, sie unter Umständen sogar einfach ignoriert.
5.3.1 Ergänzung eines Ereignisses in einer Ereignisquelle
 
Die theoretische Betrachtung eines Ereignisses soll nun an einem praktischen Beispiel gezeigt werden. Erinnern wir uns dazu zunächst an die aktuelle Implementierung der Eigenschaftsmethode Radius in der Circle-Klasse:
| public double Radius {
|
| get{return radius;}
|
| set{
|
| if(value >= 0)
|
| radius = value;
|
| else
|
| Console.Write("Unzulässiger Wert.");
|
| }
|
| }
|
Uns interessiert insbesondere der set-Accessor und dort wiederum das Verhalten der Methode, wenn der Eigenschaft ein negativer Wert übergeben wird. Nach dem derzeitigen Stand führt das zu einem Nachrichtenhinweis an der Konsole.
Die Implementierung funktioniert tadellos, unterliegt jedoch einer Einschränkung: Der Client muss die Nachricht entgegennehmen – ob er will oder nicht. Anstatt an der Konsole immer nur dieselbe, gleich lautende Meldung anzuzeigen, könnte das Circle-Objekt im Client eine Methode aufrufen. Das Objekt wird in diesem Moment aktiv, denn es löst ein Ereignis aus. Der ursprüngliche Methodenaufrufer kann als Ereignisempfänger auf das Ereignis reagieren, indem in ihm eine bestimmte Methode mit einem vom Client gewünschten Verhalten ausgeführt wird.
Bisher hatten wir es immer mit der Aufrufrichtung ausgehend von einem Client zu einem Objekt zu tun. Ein Ereignis dreht diese Richtung um. Um die Wirkungsweise der Ereignisse besser zu verstehen, wollen wir nun die Klasse Circle um ein solches Ereignis erweitern und es MeasureError nennen.
Der Programmablauf bis zu einer eventuellen Ereignisauslösung würde wie folgt aussehen:
1.
Der Benutzer, auch als Client bezeichnet, erzeugt ein Objekt der Klasse Circle und weist der Eigenschaft Radius einen unzulässigen Wert zu.
| 2. |
In der Eigenschaftsmethode wird der übergebene Wert geprüft, die Unzulässigkeit festgestellt und das Ereignis MeasureError ausgelöst mit der Folge, dass im Client nach einer Methode gesucht wird, die das Ereignis behandelt. |
|
|
|
| 3. |
Erklärt sich der Client bereit, das Ereignis zu behandeln, wird im Client die dem Ereignis zugeordnete Methode ausgeführt. |
|
|
|
Kommen wir nun zu den Details der Ereignisimplementierung in der Ereignisquelle. Jedes Ereignis muss in der Klassendefinition bekannt gegeben werden. Die allgemeine Syntax einer Ereignisdefinition lautet wie folgt:
| // Syntax der Ereignisdefinition
|
| [Zugriffsmodifizierer] event Delegattyp Eventbezeichner;
|
Dem optionalen Zugriffsmodifizierer (der Standard ist private) folgt das Schlüsselwort event, dahinter wird der Typ des Ereignisses bekannt gegeben, der immer ein Delegat ist. Weil ein Delegat den Zeiger auf eine Methode mit einer bestimmten Parameterliste und einem bestimmten Rückgabetyp beschreibt, wird damit gleichzeitig die ereignisbehandelnde Methode im Client spezifiziert. Abgeschlossen wird die Deklaration mit dem Bezeichner des Ereignisses.
In unserer Klasse Circle müssen wir demnach einen Delegaten und einen Event deklarieren, um der selbst gestellten Anforderung zu genügen:
| // ---------- Delegate ----------
|
| public delegate void MeasureErrorEventHandler();
|
| public class Circle : IDisposable {
|
| // ---------- Ereignis ----------
|
| public event MeasureErrorEventHandler MeasureError;
|
| ...
|
| }
|
Ausgangspunkt unserer Überlegungen war, bei einer unzulässigen Zuweisung an die Eigenschaft Radius eines Circle-Objekts das Ereignis MeasureError auszulösen. Der Zeitpunkt der Ereignisauslösung muss genau dann erfolgen, wenn es gewünscht wird bzw. erforderlich ist. Die Auslösung selbst ist trivial, wir brauchen dazu nur den Namen des Ereignisses anzugeben, in unserem Beispiel also:
Diese Anweisung ersetzt in der Eigenschaft Radius die Konsolenausgabe im else-Zweig des set-Accessors:
| public double Radius {
|
| get {return radius;}
|
| set {
|
| if(value >= 0)
|
| radius = value;
|
| else
|
| MeasureError();
|
| }
|
| }
|
Übergibt der Client der Eigenschaft Radius nun einen Wert, welcher der Bedingung
entspricht, wird der Delegat aktiv und sucht im Aufrufer nach einer parameterlosen Methode ohne Rückgabewert.
5.3.2 Die Behandlung eines Ereignisses im Ereignisempfänger
 
In Kenntnis davon, dass ein Circle-Objekt ein Ereignis auslösen kann, wenn der Eigenschaft Radius ein unzulässiger Wert übergeben wird, entwickeln wir zunächst eine Methode, die bei der Auslösung des Ereignisses MeasureError ausgeführt werden soll. Solche Methoden werden auch als Ereignishandler bezeichnet. Da der Typ dieses Ereignisses ein parameterloser Delegat ist, muss die Parameterliste unserer Methode natürlich leer sein.
| public void RadiusError() { }
|
Wie sich der Ereignisempfänger verhält, ob er die Ereignisauslösung ignoriert oder darauf reagiert, bleibt ihm selbst überlassen. Wir wollen in unserem Beispiel dem Anwender die Möglichkeit zu einer neuen, dann hoffentlich zulässigen Eingabe eröffnen:
| public class IrgendeineKlasse {
|
| Circle kreis = new Circle();
|
| ...
|
| public void RadiusError() {
|
| Console.WriteLine("Unzulässiger negativer Radius.");
|
| Console.Write("Neueingabe: ");
|
| kreis.Radius = Convert.ToDouble(Console.ReadLine());
|
| }
|
| }
|
Wir könnten dem Objekt kreis nun einen Radius von beispielsweise –1 zuweisen, aber die Methode RadiusError würde daraufhin nicht ausgeführt. Woher soll das Objekt auch wissen, welche Methode im Client ausgeführt werden soll, wenn das Ereignis MeasureError ausgelöst wird? Es könnten schließlich x-beliebig viele parameterlose Methoden in der Clientklasse definiert sein und grundsätzlich als Ereignishandler in Frage kommen.
Wir müssen per Anweisung den von uns bereitgestellten Handler an das Ereignis MeasureError des Objekts binden. Dazu übergeben wir dem Ereignis des Objekts mit dem »+=«-Operator eine Instanz des Delegaten MeasureErrorEventHandler unter Angabe des Bezeichners des Handlers:
| kreis.MeasureError += new MeasureErrorEventHandler(RadiusError);
|
Auf diese Weise lassen sich auch mehrere Handler hintereinander registrieren, die der Reihe nach ausgeführt werden sollen, wenn das Ereignis MeasureError ausgelöst wird. Die einzige Bedingung ist, dass jeder Handler den vom Delegaten festgelegten Kriterien hinsichtlich Parameterliste und Rückgabewert genügt.
Beachten Sie in diesem Zusammenhang auch, dass der Aufruf eines Konstruktors mit Übergabe eines unzulässigen Radius niemals dazu führen kann, das Ereignis MeasureError auszulösen, da zu diesem Zeitpunkt noch kein konkretes Objekt seitens des Ereignisempfängers existiert, das in der Lage wäre, das Ereignis wie gewünscht zu behandeln – die Bindung eines Ereignisses an ein Objekt wird erst nach dem Konstruktoraufruf wirksam.
Unser Testcode in Main könnte nun wie folgt lauten:
| public class IrgendeineKlasse {
|
| Circle kreis = new Circle();
|
| public void TestMethode(string[] args) {
|
| kreis = new Circle();
|
| kreis.MeasureError += new MeasureErrorEventHandler(RadiusError);
|
| kreis.Radius = –1;
|
| Console.ReadLine();
|
| }
|
| public void RadiusError() {
|
| Console.WriteLine("Unzulässiger negativer Radius.");
|
| Console.Write("Neueingabe: ");
|
| kreis.Radius = Convert.ToDouble(Console.ReadLine());
|
| }
|
| }
|
Führen wir Code aus, der versucht, der Eigenschaft Radius den ungültigen Wert –1 zuzuweisen, wird der Client durch die Auslösung des Ereignisses MeasureError und dem Aufruf des Handlers RadiusError von der ungültigen Zuweisung benachrichtigt. An das Ereignis ist im Clientcode die Methode RadiusError gebunden, was die Ausgabe
| Unzulässiger negativer Radius.
|
zur Folge hat. Anschließend wird der Anwender zu einer erneuten Eingabe aufgefordert. Ist die neue Eingabe korrekt, wird das Programm fortgesetzt. Ist die Eingabe hingegen wieder unzulässig, wird das Ereignis mit allen Konsequenzen erneut ausgelöst.
| Hinweis Beachten Sie, dass der Delegat in unserem Beispiel außerhalb der Klasse Circle definiert ist. Die Deklaration innerhalb der Klasse, also
class Circle : IDisposable {
public delegate void MeasureErrorEventHandler();
...
}
hätte zur Konsequenz, dass bei Erstellung des Delegatobjekts zusätzlich der Klassenbezeichner angegeben werden müsste:
kreis.MeasureError += new Circle.MeasureErrorEventHandler(RadiusError);
|
Analog zum Binden eines Ereignisses mit dem »+=«-Operator an eine Methode im Ereignisempfänger können Sie mit dem »-=«-Operator diese Bindung zu einem beliebigen Zeitpunkt wieder lösen.
5.3.3 Wenn der Ereignisempfänger ein Ereignis nicht behandelt
 
Clientseitig muss das von einem Objekt ausgelöste Ereignis nicht zwangsläufig an einen Ereignishandler gebunden werden. Legt man keinen Wert darauf, kann das Ereignis auch im Sande verlaufen, es findet dann keinen Abnehmer.
Sehen wir uns in der Klasse Circle noch einmal die Eigenschaft Radius mit dem Ereignisauslöser an:
| public double Radius {
|
| get{return radius;}
|
| set{
|
| if(value >= 0)
|
| radius = value;
|
| else
|
| MeasureError();
|
| }
|
| }
|
Die Implementierung ist noch nicht so weit vorbereitet, dass der Aufrufer das Ereignis ignorieren könnte. Wenn nämlich mit
| Circle kreis = new Circle();
|
| kreis.Radius = –2;
|
fälschlicherweise ein unzulässiger negativer Wert zugewiesen wird und das Ereignis im potenziellen Ereignisempfänger nicht behandelt wird, kommt es zur Laufzeit zu einem Fehler des Typs System.NullReferenceException. Stattdessen wird null zurückgeliefert, da der Aufruf ins Nichts führt.
Vor der Auslösung eines Ereignisses sollte daher in der Ereignisquelle zuerst geprüft werden, ob der Ereignisempfänger überhaupt die Absicht hat, das Ereignis zu verarbeiten. Mit einer if-Anweisung lässt sich das sehr einfach feststellen:
| public double Radius {
|
| get { return radius; }
|
| set {
|
| if (value >= 0)
|
| radius = value;
|
| else
|
| if (MeasureError != null)
|
| MeasureError();
|
| }
|
| }
|
5.3.4 Ereignisse mit Übergabeparameter
 
Werfen wir nun erneut einen Blick auf den Ereignishandler, der den Event MeasureError eines Circle-Objekts behandelt:
| public void RadiusError() {
|
| Console.WriteLine("Unzulässiger negativer Radius.");
|
| Console.Write("Neueingabe: ");
|
| kreis.Radius = Convert.ToSingle(Console.ReadLine());
|
| }
|
Einer kritischen Betrachtung kann die Implementierung nicht standhalten, denn wir müssen erkennen, dass der Handler keine Allgemeingültigkeit gewährleistet: Die Neueingabe des Radius wird immer dem Objekt kreis übergeben. Solange im Clientcode nur ein Circle-Objekt eine Rolle spielt, ist das bedeutungslos. Wenn jedoch mehrere Circle-Objekte das Ereignis MeasureError auslösen, müssen wir für jedes Objekt einen eigenen Ereignishandler bereitstellen. Sollte sich die Anzahl der Circle-Objekte erst zur Laufzeit ergeben, stünden wir mit der aktuellen Implementierung vor einer schier unüberwindlichen Hürde.
Das Problem ist sehr einfach zu lösen, wenn der Ereignishandler einen Parameter bereitstellt, in dem das ereignisauslösende Objekt die Referenz auf sich selbst übergibt. Darauf kann dem Radius ein neuer Wert zugewiesen werden.
| public void RadiusError(Circle sender) {
|
| Console.WriteLine("Unzulässiger negativer Radius.");
|
| Console.Write("Neueingabe: ");
|
| sender.Radius = Convert.ToSingle(Console.ReadLine());
|
| }
|
Jetzt ist der Ereignishandler so allgemein, dass er an das MeasureError-Ereignis jedes x-beliebigen Circle-Objekts gebunden werden kann. Eine Neueingabe des Radius wird immer dem ereignisauslösenden Objekt zugewiesen.
Diese Überlegung hat allerdings auch eine Änderung des Codes in der Klasse Circle zur Folge. Dazu ist zunächst die Definition des Delegaten durch einen Parameter vom Typ Circle zu ergänzen:
| public delegate void MeasureErrorEventHandler(Circle c);
|
Bei der Auslösung des Ereignisses in der Eigenschaftsmethode Radius wird dem Event MeasureError mit this die Referenz auf das aktuelle Objekt übergeben, auf das der Ereignishandler im Client Operationen ausführen kann.
| public double Radius {
|
| get{return radius;}
|
| set {
|
| if(value >= 0)
|
| radius = value;
|
| else if(MeasureError != null)
|
| MeasureError(this);
|
| }
|
| }
|
Jetzt haben wir einen Stand erreicht, der auch einer kritischen Analyse standhält: Das Ereignis MeasureError ist so allgemein definiert, dass im Client ein Ereignishandler ausreicht, um damit mehrere Circle-Objekte gleich behandeln zu können.
5.3.5 Zusammenfassung
 
|
Ein Delegat ist die Realisierung eines Funktionszeigers unter .NET und beschreibt den Zeiger auf eine beliebige Methode mit einer bestimmten Parameterliste und einem bestimmten Rückgabewert. |
|
Die Deklaration eines Delegaten erfolgt mit dem Schlüsselwort delegate. |
|
Ein Delegatobjekt kann nicht nur auf eine bestimmte, namentlich bekannte Methode verweisen. Es ist auch möglich, den auszuführenden Programmcode als anonyme Methode bereitzustellen. Definiert wird eine anonyme Methode unter Voranstellung des Schlüsselworts delegate. |
|
Ein Ereignis kehrt die Aufrufrichtung einer herkömmlichen Methode um: Sie geht vom aufgerufenen Objekt, der Ereignisquelle, zum Objektbesitzer, dem Ereignisempfänger. |
|
Ein Ereignis wird mit dem Schlüsselwort event deklariert und ist immer vom Typ eines Delegaten. Ausgelöst wird ein Ereignis in der Ereignisquelle durch die Angabe des Ereignisbezeichners. |
|
Um im Ereignisempfänger ein Ereignis zu empfangen und mit einer bestimmten Methode zu verbinden, wird eine Instanz vom Typ des Delegaten des Ereignisses erstellt, welcher der Bezeichner der auszuführenden Methode als Argument übergeben wird. Zur Verknüpfung der beiden Ausdrücke dient der »+=«-Operator, der »-=«-Operator löst die Bindung. |
|
Das von einem Objekt ausgelöste Ereignis muss nicht zwangsläufig im potenziellen Ereignisempfänger behandelt werden. Damit es zu keinem Laufzeitfehler kommt, sollte vor der Auslösung eines Ereignisses in einem Objekt mit |
Ereignisbezeichner != null
geprüft werden, ob sich der Empfänger bereit erklärt, das Ereignis zu verarbeiten. |